本系列文章已重新編修,並在加入部分 ES6 新篇章後集結成書,有興趣的朋友可至天瓏書局選購,感謝大家支持。
購書連結 https://www.tenlong.com.tw/products/9789864344130
讓我們再次重新認識 JavaScript!
如果要說 JavaScript 最核心也最容易被誤用的部分,我想應該就非函式 (Function) 莫屬了,那麼在接下來的分享中,我們就來聊聊關於函式的部分。
在前面介紹變數型別的時候曾經說過,除了基本型別以外的都是物件。
當我們透過 typeof 去檢查一個「函式 (function) 」的時候,雖然你會得到 "function" 的結果,讓你以為 function 也是 JavaScript 定義的一種型別,但實際上它仍屬於 Object 的一種。
你可以把它想像成是一種可以被呼叫 (be invoked) 的特殊物件 (值)。
「函式」指的是將一或多段程式指令包裝起來,可以重複使用,也方便維護。
宣告函式的方法有好幾種,但不管是什麼方式,通常一個函式會包含三個部分:
( ) 中的部分,稱為「參數 (arguments) 」,參數與參數之間會用逗號 , 隔開{ } 內的部分,內含需要重複執行的內容,是函式功能的主要區塊。例如:
function square(number) {
  return number * number;
}
square(2);        // 4
square(3);        // 9
square(4);        // 16
以上是一個函式的宣告與呼叫簡單示範。
函式使用 function 關鍵字來宣告名稱,參數 number 位於括號之中。
於是透過執行 square(2); 來呼叫 square 函式,此時 square 函式裡面的 number 的值就會是傳進來的 2,而 number * number 的結果就會是 4 了。
最後再透過 return 回傳結果,如果沒有使用 return 回傳,則預設會回傳 undefined。
常見定義函式的方式有這幾種:
new Function 關鍵字建立函式下面我們一一介紹。
「函式宣告」應該是屬於最常見的用法:
function 名稱([參數]) {
  // 做某事
}
像本篇一開始的範例就是用這種方式:
function square(number) {
  return number * number;
}
另一種方式,則是透過 變數名稱 = function([參數]){ ... };  的方式,將一個函式透過 = 指定給某個變數。
像這樣:
var square = function (number) {
  return number * number;
};
可能有些人會覺得這樣很奇怪,但還記得我們一直強調的嗎?
函式實際上它仍屬於 Object 的類型,是一種可以被呼叫 (be invoked) 的特殊物件 (值),自然可以透過變數存入囉。
是的,聰明的你也許已經察覺到了,在範例裡 = 後面的 function 是「沒有名字」的:
var square = function (number) {
  return number * number;
};
像這類沒有名字的函式在 JavaScript 是合法的,通常我們會稱它為「匿名函式」。
匿名函式我們等等還會見到,現在先介紹到這裡。
在函式運算式中,如果想要在 function 後面加上一個名字是可以的嗎?
可以,像這樣:
var square = function func(number) {
  return number * number;
};
但是要注意的是,這個名字只在「自己函式的區塊內」有效,也就是說:
var square = function func(number) {
  console.log( typeof func );   // "function"
  return number * number;
};
console.log( typeof func );     // undefined
像這樣,脫離了函式自身區塊後,變數 func 就不存在了。
當然,在「匿名函式」的函式運算式情況下,你還是可以透過自定義的變數名稱取得 function,沒有一定要替這個函式取名的理由:
var square = function func(number) {
  console.log( typeof square );   // "function"
  return number * number;
};
new Function 關鍵字建立函式最後一種方式就是直接使用 Function (注意 F 大寫) 這個關鍵字來建立函式物件。 使用時將參數與函式的內容依序傳入 Function,就可以建立一個函式物件了。 像這樣:
// 透過 new 來建立 Function "物件"
var square = new Function('number', 'return number * number');
透過 new Function 所建立的函式物件,每次執行時都會進行解析「字串」(如 'return number * number' ) 的動作,運作效能較差,所以通常實務上也較少會這樣做。
但不管是透過哪一種方式定義函式,呼叫函式的話就直接用「函式名稱(參數)」的方式,像 square(2); 就可以了。 [註1]
終於要講到全域變數與區域變數的差異了。
在 ES6 之前,JavaScript 變數有效範圍的最小單位是以 function 做分界的。 [註2]
什麼意思呢? 讓我用簡單的範例來說明:
var x = 1;
var doSomeThing = function(y) {
  var x = 100;
  return x + y;
};
console.log( doSomeThing(50) );   // ?
console.log( x );                 // ?
猜猜看,這兩組 console.log() 分別會印出什麼?
.
.
.
答案是 150 與 1。
由於函式 doSomeThing() 裡面再次定義了變數 x,所以當我們執行 doSomeThing(50) 時,會將 50 作為參數傳入 doSomeThing() 的 y,那麼 return x + y 的結果自然就是 100 + 50 的 150 了。
那麼下一行再印出的 x 呢? 為什麼是 1 而不是 100 ?
因為...
「切分變數有效範圍的最小單位是 "function" 」
「切分變數有效範圍的最小單位是 "function" 」
「切分變數有效範圍的最小單位是 "function" 」
很重要,所以要講三次。
因為切分變數有效範圍的最小單位是 "function",所以在函式區塊內透過 var 定義的 x 實際上只屬於這個函式。
換句話說,外面的 x 跟 function 內的 x 其實是兩個不同的變數。
因此在最後印出來的 console.log( x ); 自然就是外面的 x 也就是 1 了。
所以我們說,變數有效範圍的最小單位是 "function", 這個有效範圍我們通常稱它為「Scope」。
那麼,如果 function 內部沒有 var x 呢?
很簡單,自己的 function 內如果找不到,就會一層層往外找,直到全域變數為止:
var x = 1;
var doSomeThing = function(y) {
  // 內部找不到 x 就會到外面找,直到全域變數為止。
  // 都沒有就會報錯:ReferenceError: x is not defined
  return x + y;
};
console.log( doSomeThing(50) );   // 51
要注意的是, function 可以讀取外層已經宣告的變數,
但外層拿不到裡面宣告的變數。
var 宣告的變數很危險!「沒有 var 宣告的變數很危險」什麼意思?
來,稍微修改一下剛剛的範例,把 function 內的 var 拿掉:
var x = 1;
var doSomeThing = function(y) {
  x = 100;
  return x + y;
};
console.log( doSomeThing(50) );   // ?
console.log( x );                 // ?
猜猜看,這兩組 console.log() 分別會印出什麼?
.
.
.
答案是  ...... 才。不。是。勒~~150 與 1
答案是 150 與 100。

先別急著崩潰,剛剛說過「切分變數有效範圍的最小單位是 "Function" 」對吧?
但這句話的前提是你得在 function 內部再次用 var 宣告這個變數,否則 JavaScript 會再往外層去找到同名的變數,直到最外層,也就是「全域變數」。
換言之,由於在 function 內沒有重新宣告 x 變數,使得 x = 100 跑去變更了外層的同名變數 x:
var doSomeThing = function(y) {
  x = 100;
  return x + y;
};
導致在呼叫 doSomeThing(50) 之後再印出 x 的值自然就變成 100 囉。
覺得混亂了嗎? 還沒完呢。
現在我們把 var 加回去,然後在上面加一行 console.log(x) 像這樣:
var x = 1;
var doSomeThing = function(y) {
  console.log(x);   // 會出現什麼?
  var x = 100;
  return x + y;
};
console.log( doSomeThing(50) );   // 150
console.log( x );                 // 1
現在我們已經知道 doSomeThing(50) 與 x 的值是 150 以及 1 了,
那麼要讓各位來猜猜看,在 function 內的 console.log(x) 會出現什麼?
.
.
.
答案是 1 或 100 嗎? (打叉)
再猜一次。
.
.
.
正確答案是 undefined。

醒醒啊,天還沒黑,別急著睡覺。
其實啊,剛剛那份程式碼在瀏覽器 (或者編譯器) 的眼中,是長這樣的:
var x = 1;
var doSomeThing = function(y) {
  var x;
  console.log(x);   // 會出現什麼?
  x = 100;
  return x + y;
};
console.log( doSomeThing(50) );   // 150
console.log( x );                 // 1
看出差異了嗎?
雖然我們這次在函式內部有透過 var 對變數 x 來重新做宣告,但是呢,要是不小心在 var 宣告前就使用了這個變數,這時候 JavaScript
就會開始尋找變數 x 了,在自己的 scope 找... 啊,找到了!
雖然是在下面,但可以確認的是自己的 scope 裡面有宣告,於是就 很貼心地 「只會把宣告的語法」拉到這個 scope 的「最上面」...
(還記得前面介紹變數時講過的嗎? 只要變數有被宣告,使用時就不會有錯誤,否則會出現 ReferenceError 的錯誤。)
最後就變成這個樣子:
var doSomeThing = function(y) {
  var x;
  console.log(x);   // undefined
  x = 100;
  return x + y;
};
而 JavaScript 的這種特性,我們稱作「變數提升」 (Variables Hoisting)。 [註3]
也因為這種奇怪特性的關係,強烈建議所有可能用到的變數都盡量在 scope 的最上面先宣告完成後再使用。
除了變數以外,函式有沒有提升? 答案是有。
還記得本文一開始說過,函式的定義有分成幾種,其中也可以分成 var xxx = function() {...} 存入變數的「函式運算式」以及直接用 function xxx() {...} 定義的「函式宣告」對吧?
這兩種定義方式最大的差別在於,透過「函式宣告」方式定義的函式可以在宣告前使用 (函式提升) :
square(2);    // 4
function square(number) {
  return number * number;
}
而透過「函式運算式」定義的函式則是會出現錯誤:
square(2);    // TypeError: square is not a function
var square = function (number) {
  return number * number;
};
與變數提升的差別在於變數提升只有宣告被提升,而函式的提升則是包括內容完全被提升。 除了可呼叫的時機不同外,「函式宣告」與「函式運算式」在執行時期兩者無明顯差異。
看到這裡,相信你應該對變數的作用範圍有了基本的理解對吧,在本文的最後我再針對「全域變數」與「區域變數」做一些補充說明。
其實在 JavaScript 這門語言中,沒有所謂「全域變數」這種東西。
更準確地說,我們所說的「全域變數」其實指的是「全域物件」(或者叫「頂層物件」) 的屬性。
以瀏覽器來說,「全域物件」指的就是 window,在 node 環境中則叫做 global。
舉個例子,我們在最外層透過 var 建立一個變數 a,像這樣:
var a = 10;
一直以來我們都稱它叫「全域變數」對吧?
這個時候,請你在後面加一行:
var a = 10;
console.log( window.a );    // ?
看到了什麼?
這時你應該會看到剛剛指定給 a 的 10 這個數字才對。
那麼就可以來下個結論:
function (ES6 的 let 與 const 例外)var 的變數會變成「全域變數」所以看到這裡,相信你應該對「全域變數」與「區域變數」有了更直接的理解吧!
最後分享一下,這是我在網友推特上看到的:
以後有人問你類似問題,相信你也可以抬頭挺胸自信地回答他囉!
來源: https://twitter.com/rayshih771012/status/930075889483726849
[註1] 函式呼叫:除了單純的 函式() 之外,還有 .call() 與 .apply(),在後續的篇章介紹 this 時會提到這些。
[註2] ES6 之後有 let 與 const 分別定義「變數」與「常數」。 與 var 不同的是,它們的 scope 是透過大括號 { } 來切分的。
[註3] 提升:提升看起來是將變數和函數的宣告移動到程式區塊頂端,但實際上是變數和函數的宣告會在編譯階段中先被放入記憶體,實際在程式碼中位置還是一樣,往上移動的說法是為了幫助理解。
花了好幾天的時間,「重新認識 JavaScript」JS 基礎篇終於告一段落了,各位對 JavaScript 有了基本的理解之後,接著我們要開始進入瀏覽器的部分了。
在接下來的部分都會為各位詳細的介紹。
下一篇:前端工程師的主戰場:瀏覽器裡的 JavaScript,我們明天見。

想請問為什麼 印出的aaa是最後跑完的
because object is call by reference.
var a = new Array();
var b = new Object();
for( let i = 0; i < 5; i++ ){
  b.x = i;
  console.log( b );
}
and it will print
{ x: 0 }
{ x: 1 }
{ x: 2 }
{ x: 3 }
{ x: 4 }
意思是  迴圈跑完到最後一個"4"
但是aaa每個索引都是引用到這個最後跑完的4 的意思嗎
可是  如果要得到正確的aaa要如何解決?
有試過你另一篇說的  匿名函式包起來的方法  但是不知是我用錯
還是本來就不是這樣用
IIFE 那篇與你的問題是兩件事。
你 push 到 aaa 的物件,從 i = 0 到 i = 4 這五次 push 進去的都是同一個 bbb 物件,每執行一次迴圈, bbb.x 就會被更新成新的數值。
如果你希望每次 push 進去都是新的物件,那麼就在 for 迴圈內建立一個新的物件吧:
var a = new Array();
for( let i = 0; i < 5; i++ ){
  let b = new Object();
  b.x = i;
  a.push( b );
}
console.log(a);
謝謝你
抱歉  這個問題我留錯地方了
Kuro 大大您好
想請教關於函式作用域問題,我改寫ㄧ下您文中的範例
var x = 1;
var doSomeThing = function(y) {
  x = 100;
  return x + y;
};
console.log( x ); //1
console.log( doSomeThing(50) );   //150
我好奇為何console.log(x),印出來1的結果.
我自己想了一下原因,是否為:JS運作是從上到下解析,因此還未執行到dosomething這個函式,所以函式中未宣告的變數x,並沒有跑去變更外層的變數x?
你好,在未呼叫 doSomeThing() 這個 function 以前,裡面的程式碼都不會被執行,所以 x 當然就是原本設定的 1 了。
你可以試試在你提供的程式碼執行的最後再加一行:
var x = 1;
var doSomeThing = function(y) {
  x = 100;
  return x + y;
};
console.log( x ); // 1
console.log( doSomeThing(50) );   // 150
console.log( x );  // 1 or 100 ?
最後的 x 會是 1 或 100 ?
在 doSomeThing 裡頭的 x = 100; 改成 var x = 100; 後,又會有什麼不同 :)
原來如此,我懂了!謝謝kuro大大
Kuro 老師 你好:
正在研讀您的大作,有些觀念想和您確認一下,所以就來了鐵人賽相同的篇章下發問,
// 若是在全域的狀況下
var a = 10;   //使用 var 宣告
b = 10;    // 不使用 var
// 是不是都是一樣的宣告方式?因為
console.log(a);   // 10
console.log(b);   // 10
console.log(window.a);   // 10
console.log(window.b);   // 10
在 window 下也都能找到 a 與 b 這兩個變數
所以是不是代表在全域的狀況下,有沒有使用 var 宣告變數都是一樣的
而若是在 function 的 {} 內宣告變數,
使用 var 宣告,則可以將變數限制在 {} 內,不會汙染全域,
而若是在 {} 內宣告變數不使用 var 的話,也是會跳出 {} 的範圍汙染到全域,
請問我以上的觀念是否正確,請不吝指正,謝謝
沒錯。 在全域的環境下,不管有沒有透過 var 宣告,這個變數都是全域的。
在 function (){ } 範圍內,若在自己的 scope 沒有宣告變數,則會一層層往上找,直到最外層 (全域) 為止。
a = 1000;
(function (){
  var a = 10;
  (function (){
    a = 20;
    console.log(a);
  })()
  console.log(a);
})()
console.log(a);
像這種情況,雖然內層的 a = 20 沒有 var 但它也不是全域的變數。
那是因為內層的 a = 20 重新賦值外層原本的 var a = 10,這樣對嗎?
我將這段程式放進了 Console,顯示為 20 20 1000
若是最內層的 a = 20 也有 var 的話,
那 Console 就會變成 20 10 1000 了,
這樣是否正確呢?謝謝指教~
是的

謝謝 Kuro 老師
嗚嗚嗚...我真的是JS中文名詞苦手QQ
什麼函式、函數、參數、物件屬性、匿名函式等等等等...搞的頭昏腦花的QQ
好險有Kuro大大寫重新認識JS,謝謝Kuro大大真的幫助我很多... (跪Orz
寫英文也可以啊 XD
var x = 1;
var doSomeThing = function(y) {
  var x;
  console.log(x);   // 會出現什麼?
  x = 100;
  return x + y;
};
console.log( doSomeThing(50) ); 
要是不小心在 var 宣告前就使用了這個變數,這時候 JavaScript 就會開始尋找變數 x 了,在自己的 scope 找...
這一段我不太理解想請問「var 宣告前」這句話的意思,因為不是已經有宣告var x = 1;了,還是說是指 function 裡面的宣告?如果不是指後者的意思的話,不知道我下面的理解是否正確?
我的理解是:當執行到 console.log( doSomeThing(50) ); 的時候,JS 會已經從最上方開始找到 var x = 1,然後再執行 doSomeThing()。
因為 doSomeThing() 裡面先用到了console.log(x);然後又再次宣告 x = 100 ,所以發生 hoisting.  可是這樣的話,跟您提到的「var 宣告前」好像又沒有關係了@@,因為我認為這樣的情況是因為「再次宣告」才造成 hoisting.
你好, JavaScript var 變數的作用範圍 (scoped) 為 function,所以此範例可以無視外面的 var x = 1;。
以此範例來說,除非在 function 內沒有加上 var x = 100; 這行,那麼 function 內的 x 與外面的 x 就會是同一個。
var x = 1;
var doSomeThing = function(y) {
  // 此即「宣告前」
  console.log(x);   // 會出現什麼?
  
  // 此處透過 var 宣告 x 變數
  var x = 100;
  return x + y;
};
console.log( doSomeThing(50) ); 
想請問~
我新增一個.js檔然後使用vscode做編輯
但是當我直接這樣宣告
a = 10
卻印出a is not defined的error呢?
但是在jsbin上卻正常輸出10。
你好,因為我看不到你的環境,所以無法判斷你的問題出在哪
可以請你附上截圖說明你的問題嗎?

你好,因為 node 環境下不像瀏覽器會將所有未宣告的變數納入 window 這個全域物件下,所以當你在 node 嘗試設定一個尚未宣告的變數 a 就會出現像這樣的訊息。
而在瀏覽器環境下,這個未宣告的變數 a 會自動變成 window 這個全域物件下的屬性,也就是俗稱的全域變數,意義等同於 window.a 。
明白了~
感謝酷囉大大的回覆!!!